查看原文
其他

遇到Android12系统上的一个“误区”

小迪vs同学 鸿洋 2023-09-13

本文作者


作者:小迪vs同学

链接:

https://juejin.cn/post/7244809769784590373

本文由作者授权发布。


1问题背景


在Android12平台上,恢复出厂设置后,已使用空间偏高,空间使用率为28/128=21.9%,产品需要控制在11%以内。


2源码分析


当前页面的源码位于packages/apps/Settings/src/com/android/settings/deviceinfo/StorageDashboardFragment.java
/**
 * VolumeSizeCallbacks exists because StorageCategoryFragment already implements
 * LoaderCallbacks for a different type.
 */

public final class VolumeSizeCallbacks
        implements LoaderManager.LoaderCallbacks<PrivateStorageInfo> 
{
    @Override
    public Loader<PrivateStorageInfo> onCreateLoader(int id, Bundle args) {
        final Context context = getContext();
        final StorageManagerVolumeProvider smvp =
                new StorageManagerVolumeProvider(mStorageManager);
        final StorageStatsManager stats = context.getSystemService(StorageStatsManager.class);
        //使用loader技术加载存储卷信息
        return new VolumeSizesLoader(context, smvp, stats,
                mSelectedStorageEntry.getVolumeInfo());
    }

    @Override
    public void onLoaderReset(Loader<PrivateStorageInfo> loader) {
    }

    @Override
    public void onLoadFinished(
            Loader<PrivateStorageInfo> loader, PrivateStorageInfo privateStorageInfo) 
{
        if (privateStorageInfo == null) {
            getActivity().finish();
            return;
        }
        //loader加载完毕,返回存储卷信息
        mStorageInfo = privateStorageInfo;
        //更新UI
        onReceivedSizes();
    }
}

private void onReceivedSizes() {
    if (mStorageInfo == null || mAppsResult == null) {
        return;
    }

    if (getView().findViewById(R.id.loading_container).getVisibility() == View.VISIBLE) {
        setLoading(false /* loading */true /* animate */);
    }
    //已用空间=总空间-可用空间
    final long privateUsedBytes = mStorageInfo.totalBytes - mStorageInfo.freeBytes;
    //绑定ui
    mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo());
    mPreferenceController.setUsedSize(privateUsedBytes);
    mPreferenceController.setTotalSize(mStorageInfo.totalBytes);
    for (int i = 0, size = mSecondaryUsers.size(); i < size; i++) {
        final AbstractPreferenceController controller = mSecondaryUsers.get(i);
        if (controller instanceof SecondaryUserController) {
            SecondaryUserController userController = (SecondaryUserController) controller;
            userController.setTotalSize(mStorageInfo.totalBytes);
        }
    }
    //更新分类item
    mPreferenceController.onLoadFinished(mAppsResult, mUserId);
    updateSecondaryUserControllers(mSecondaryUsers, mAppsResult);
    setSecondaryUsersVisible(true);
}
VolumeSizesLoader继承自AsyncTaskLoader,主要是要看VolumeSizesLoader是如何加载存储卷信息的。
@Override
public PrivateStorageInfo loadInBackground() {
    PrivateStorageInfo volumeSizes;
    try {
        //子线程加载
        volumeSizes = getVolumeSize(mVolumeProvider, mStats, mVolume);
    } catch (IOException e) {
        return null;
    }
    return volumeSizes;
}

@VisibleForTesting
static PrivateStorageInfo getVolumeSize(
        StorageVolumeProvider storageVolumeProvider, StorageStatsManager stats, VolumeInfo info)
        throws IOException 
{
    //使用StorageVolumeProvider直接获取
    long privateTotalBytes = storageVolumeProvider.getTotalBytes(stats, info);
    long privateFreeBytes = storageVolumeProvider.getFreeBytes(stats, info);
    return new PrivateStorageInfo(privateFreeBytes, privateTotalBytes);
}
StorageVolumeProvider是一个接口,主要作用就是提供对存储卷的访问,其实现类为StorageManagerVolumeProvider
@Override
public long getTotalBytes(StorageStatsManager stats, VolumeInfo volume) throws IOException {
    return stats.getTotalBytes(volume.getFsUuid());
}

@Override
public long getFreeBytes(StorageStatsManager stats, VolumeInfo volume) throws IOException {
    return stats.getFreeBytes(volume.getFsUuid());
}
@WorkerThread
public @BytesLong long getTotalBytes(@NonNull UUID storageUuid) throws IOException {
    try {
        return mService.getTotalBytes(convert(storageUuid), mContext.getOpPackageName());
    } catch (ParcelableException e) {
        e.maybeRethrow(IOException.class);
        throw new RuntimeException(e);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

@WorkerThread
public @BytesLong long getFreeBytes(@NonNull UUID storageUuid) throws IOException {
    try {
        return mService.getFreeBytes(convert(storageUuid), mContext.getOpPackageName());
    } catch (ParcelableException e) {
        e.maybeRethrow(IOException.class);
        throw new RuntimeException(e);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}
可以看到最终调用了远程StorageStatsService的功能。
@Override
public long getTotalBytes(String volumeUuid, String callingPackage) {
    // NOTE: No permissions required

    if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) {
        //代码1,获取基础存储大小并格式化返回(经调试,执行这里的逻辑)
        return FileUtils.roundStorageSize(mStorage.getPrimaryStorageSize());
    } else {
        final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid);
        if (vol == null) {
            throw new ParcelableException(
                    new IOException("Failed to find storage device for UUID " + volumeUuid));
        }
        Log.d("jasonwan""vol.disk.sysPath:"+vol.disk.sysPath+", vol.disk.size:"+vol.disk.size);
        return FileUtils.roundStorageSize(vol.disk.size);
    }
}

@Override
public long getFreeBytes(String volumeUuid, String callingPackage) {
    // NOTE: No permissions required

    final long token = Binder.clearCallingIdentity();
    try {
        final File path;
        try {
            //获取对应存储卷的路径
            path = mStorage.findPathForUuid(volumeUuid);
            Log.d("jasonwan""free path: "+path.getAbsolutePath()+", and size is:"+path.getUsableSpace());
        } catch (FileNotFoundException e) {
            throw new ParcelableException(e);
        }

        // Free space is usable bytes plus any cached data that we're
        // willing to automatically clear. To avoid user confusion, this
        // logic should be kept in sync with getAllocatableBytes().
        if (isQuotaSupported(volumeUuid, PLATFORM_PACKAGE_NAME)) {
            final long cacheTotal = getCacheBytes(volumeUuid, PLATFORM_PACKAGE_NAME);
            final long cacheReserved = mStorage.getStorageCacheBytes(path, 0);
            final long cacheClearable = Math.max(0, cacheTotal - cacheReserved);
            Log.d("jasonwan""cacheClearable size: "+cacheClearable);

            return path.getUsableSpace() + cacheClearable;
        } else {
            //返回路径可用空间大小
            return path.getUsableSpace();
        }
    } finally {
        Binder.restoreCallingIdentity(token);
    }
}
看看mStorage.getPrimaryStorageSize()的代码。
/** {@hide} */
public long getPrimaryStorageSize() {
    File dataDirectory = Environment.getDataDirectory();
    File rootDirectory = Environment.getRootDirectory();
    //代码2
    long total = FileUtils.roundStorageSize(dataDirectory.getTotalSpace() + rootDirectory.getTotalSpace());
    Log.d("jasonwan""Environment.getDataDirectory():"+ dataDirectory +", totalSpace:"+ dataDirectory.getTotalSpace()+", freeSpace:"+dataDirectory.getFreeSpace()+", usedSpace:"+(dataDirectory.getTotalSpace()-dataDirectory.getFreeSpace()));
    Log.d("jasonwan""Environment.getRootDirectory():"+ rootDirectory+", totalSpace:"+rootDirectory.getTotalSpace()+", freeSpace:"+rootDirectory.getFreeSpace()+", usedSpace:"+(rootDirectory.getTotalSpace()-rootDirectory.getFreeSpace()));
    Log.d("jasonwan","calculate total:"+total+", real total:"+(dataDirectory.getTotalSpace() + rootDirectory.getTotalSpace()));
    Log.d("jasonwan","real free:"+(dataDirectory.getFreeSpace()+rootDirectory.getFreeSpace()));
    return total;
}
总大小就是获取的/data目录和/system目录的总大小,这里通过自定义log,打印出实际路径。
05-06 00:48:54.353  1222  4361 D jasonwan: Environment.getDataDirectory():/datatotalSpace:101534478336freeSpace:97527693312usedSpace:4006785024
05-06 00:48:54.353  1222  4361 D jasonwan: Environment.getRootDirectory():/systemtotalSpace:2145386496freeSpace:1651765248usedSpace:493621248
05-06 00:48:54.353  1222  4361 D jasonwan: calculate total:128000000000, real total:103679864832
05-06 00:48:54.353  1222  4361 D jasonwan: real free:99179458560
而可用空间的大小就是获取的/data目录的可用大小,同样,这里通过自定义log,打印出实际路径。
05-06 00:48:54.358  1222  2079 D jasonwan: free path: /data, and size is:97393475584
05-06 00:48:54.481  1222  4361 D jasonwan: cacheClearable size: 0
这里有个很奇怪的地方,就是计算出来的byte大小经过FileUtils.roundStorageSize()或者Formatter.formatBytes()格式化后变大了,代码1处,mStorage.getPrimaryStorageSize()为103679864832,经过FileUtils.roundStorageSize()格式化后变成了128000000000,来看下FileUtils.roundStorageSize()的计算原理。
/**
 * Round the given size of a storage device to a nice round power-of-two
 * value, such as 256MB or 32GB. This avoids showing weird values like
 * "29.5GB" in UI.
 *
 * @hide
 */

public static long roundStorageSize(long size) {
    long val = 1;
    long pow = 1;
    while ((val * pow) < size) {
        val <<= 1;
        if (val > 512) {
            val = 1;
            pow *= 1000;
        }
    }
    return val * pow;
}
根据方法注释说明,该方法将通过四舍五入,将参数值变成接近的2的n次幂,比如:
  • 100->128
  • 129->256
  • 257->512
  • 513->1000
注意,最后不是1024,而是1000,这个跟我们实际生活是很贴近的,比如我们去买一个U盘,老板说有2G的,4G的,32G的,但绝不会说有3G的,3.5G的。可见此格式化方法最终会将真实数值变大。而Formatter.formatBytes()在将Bytes变成KB、MB、GB时,会以1000作为计量单位,而不是1024。
/** {@hide} */
@UnsupportedAppUsage
public static BytesResult formatBytes(Resources res, long sizeBytes, int flags) 
{
    //这里unit的值最终等于1000,不是1024
    final int unit = ((flags & FLAG_IEC_UNITS) != 0) ? 1024 : 1000;
    final boolean isNegative = (sizeBytes < 0);
    float result = isNegative ? -sizeBytes : sizeBytes;
    int suffix = com.android.internal.R.string.byteShort;
    long mult = 1;
    if (result > 900) {
        suffix = com.android.internal.R.string.kilobyteShort;
        mult = unit;
        result = result / unit;
    }
    if (result > 900) {
        suffix = com.android.internal.R.string.megabyteShort;
        mult *= unit;
        result = result / unit;
    }
    if (result > 900) {
        suffix = com.android.internal.R.string.gigabyteShort;
        mult *= unit;
        result = result / unit;
    }
    if (result > 900) {
        suffix = com.android.internal.R.string.terabyteShort;
        mult *= unit;
        result = result / unit;
    }
    if (result > 900) {
        suffix = com.android.internal.R.string.petabyteShort;
        mult *= unit;
        result = result / unit;
    }
    // Note we calculate the rounded long by ourselves, but still let String.format()
    // compute the rounded value. String.format("%f", 0.1) might not return "0.1" due to
    // floating point errors.
    final int roundFactor;
    final String roundFormat;
    if (mult == 1 || result >= 100) {
        roundFactor = 1;
        roundFormat = "%.0f";
    } else if (result < 1) {
        roundFactor = 100;
        roundFormat = "%.2f";
    } else if (result < 10) {
        if ((flags & FLAG_SHORTER) != 0) {
            roundFactor = 10;
            roundFormat = "%.1f";
        } else {
            roundFactor = 100;
            roundFormat = "%.2f";
        }
    } else { // 10 <= result < 100
        if ((flags & FLAG_SHORTER) != 0) {
            roundFactor = 1;
            roundFormat = "%.0f";
        } else {
            roundFactor = 100;
            roundFormat = "%.2f";
        }
    }

    if (isNegative) {
        result = -result;
    }
    final String roundedString = String.format(roundFormat, result);

    // Note this might overflow if abs(result) >= Long.MAX_VALUE / 100, but that's like 80PB so
    // it's okay (for now)...
    final long roundedBytes =
            (flags & FLAG_CALCULATE_ROUNDED) == 0 ? 0
            : (((long) Math.round(result * roundFactor)) * mult / roundFactor);

    final String units = res.getString(suffix);

    return new BytesResult(roundedString, units, roundedBytes);
}

综上,真实总大小103679864832先换算为128000000000,然后格式为GB即为128GB,也就是文章开头我们看到的总大小128GB,已使用空间同理。


3解决方案


那如果我们使用真实的bytes大小来计算空间使用率,则为1-97393475584/103679864832=6.1%,远低于界面显示的21.9%,同时也符合产品的需求。所以这只是计算误差问题,实际使用率并没有那么高。


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

关于Jetpack DataStore的八点疑问
Android 14 之返回界面升级:预览目标界面 + 全新返回箭头
我们写的build.gradle是如何跑起来的? -  Gradle探究


点击 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存